02 - Elementy programowania obiektowego

Podstawy przetwarzania danych

Politechnika Poznańska, Instytut Robotyki i Inteligencji Maszynowej

Ćwiczenie laboratoryjne 2: elementy programowania obiektowego

Powrót do spisu treści ćwiczeń laboratoryjnych


Programowanie obiektowe

Programowanie obiektowe (ang. object-oriented programming, OOP) – paradygmat programowania, w którym programy definiuje się za pomocą obiektów – elementów łączących stan (czyli dane, nazywane najczęściej atrybutami) i zachowanie (czyli procedury, tu: metody). Obiektowy program komputerowy wyrażony jest jako zbiór takich obiektów, komunikujących się pomiędzy sobą w celu wykonywania zadań.

Podejście to różni się od tradycyjnego programowania proceduralnego, gdzie dane i procedury nie są ze sobą bezpośrednio związane. Programowanie obiektowe ma ułatwić pisanie, konserwację i wielokrotne użycie programów lub ich fragmentów. [źródło]

Klasy i obiekty

Python jest językiem programowania wspierającym paradygmat programowania obiektowego. W Pythonie wszystko jest obiektami: liczby, listy, słowniki itd. Klasy z kolei, są podstawowym elementem programowania obiektowego. Pozwalają na połączenie danych i operacji na tych danych w jedną całość. Dodanie własnej klasy pozwala na zdefiniowanie nowego typu danych, który będzie miał określone atrybuty i metody (metody są odpowiednikami funkcji w klasach).

Utworzenie klasy w języku Python jest bardzo proste. Wystarczy użyć słowa kluczowego class i podać nazwę klasy. Wewnątrz klasy można zdefiniować atrybuty (zmienne) i metody (funkcje).

class Character:
    name = "John the Brave"
    health = 100
    items = ["sword", "shield"]

    def introduce(self):
        print("Hello!")

Powyższa klasa Character posiada trzy atrybuty: name, health i items oraz jedną metodę introduce. Zwróć uwagę, że metoda introduce przyjmuje argument self. Argument self jest referencją do obiektu, na którym wywołano metodę. Dzięki temu metoda może odwoływać się do atrybutów obiektu (więcej przykładów będzie niżej).

Aktualnie klasa Character posiada trzy zmienne, które są wspólne dla wszystkich obiektów tej klasy, co nie jest pożądane. Dobrze jest ustawiać te zmienne w konstruktorze klasy. Konstruktor klasy to specjalna metoda __init__, która jest wywoływana podczas tworzenia nowego obiektu. W konstruktorze można przekazać argumenty, które będą inicjalizować atrybuty obiektu.

class Character:
    def __init__(self, name, health, items):
        self.name = name
        if health > 0: 
            self.health = health
        else: # Postać nie może mieć mniej niż 0 punktów życia
            self.health = 0
        self.health = health
        self.items = items

    def introduce(self):
        print("Hello!")

    def is_alive(self):
        return self.health > 0

Zauważ, że w konstruktorze zamiast name, health i items używamy self.name, self.health i self.items. Tak samo będziemy się odwoływać do tych atrybutów w pozostałych metodach klasy. Dodatkowo, zdefiniowaliśmy nową metodę is_alive, która zwraca True, jeśli postać ma więcej niż 0 punktów życia albo False w przeciwnym przypadku.

Utworzenie obiektu klasy odbywa się poprzez wywołanie nazwy klasy jak funkcji. W przypadku klasy Character wygląda to tak:

character1 = Character("John the Brave", 100, ["sword", "shield"])

Możliwe jest również modyfikowanie atrybutów obiektu:

character1 = Character("John the Brave", 100, ["sword", "shield"])
print(character1.name)  # John the Brave
character1.name = "John the Wise"
print(character1.name)  # John the Wise

💥 Zadanie 1 💥

Zmodyfikuj metodę introduce tak, aby wypisywała imię i przedmioty postaci, np.:

Hello! My name is John the Brave and I have: 
- sword 
- shield

Wywołaj metodę introduce na obiekcie character1, następnie zmień imię postaci na “John the Wise” i wywołaj metodę introduce ponownie.

Podpowiedź

Metoda introduce powinna iterować po liście przedmiotów i wypisywać je na ekran:

for item in self.items:

Wyświetlanie zmiennych korzystając z f-stringów:

print(f"Hello! My name is {self.name} and I have:")

💥 Zadanie 2 💥

Dodaj do klasy Character metodę take_damage, która przyjmuje jako argument ilość punktów obrażeń. Metoda powinna odjąć tę wartość od punktów życia postaci. Jeśli po odjęciu obrażeń postać ma mniej niż 0 punktów życia, to metoda powinna ustawić punkty życia na 0.

Operatory

Python pozwala na przeciążanie operatorów. Oznacza to, że można zdefiniować własne zachowanie operatorów dla obiektów danej klasy. Przykładowo, jeśli chcemy dodać dwa obiekty klasy ComplexNumber, to możemy zdefiniować zachowanie operatora +, poprzez zdefiniowanie metody __add__ (dwa znaki _ przed i po add, takie metody nazywa się magicznymi, ang. magic methods). :

class ComplexNumber:
    def __init__(self, real, imag):
        self.real = real
        self.imaginary = imag

    # Zdefiniowanie metody __add__ pozwala na dodawanie dwóch obiektów klasy ComplexNumber korzystając z operatora +
    def __add__(self, other): 
        # Zwracamy nowy obiekt klasy ComplexNumber, z częścią rzeczywistą równą sumie części rzeczywistych obu obiektów i częścią urojoną równą sumie części urojonych obu obiektów
        return ComplexNumber(self.real + other.real, self.imaginary + other.imaginary) 

Metody magiczne

Operator Metoda Znaczenie
+ __add__ Dodawanie
- __sub__ Odejmowanie
* __mul__ Mnożenie
/ __truediv__ Dzielenie
// __floordiv__ Dzielenie całkowite
% __mod__ Reszta z dzielenia
** __pow__ Potęgowanie
== __eq__ Równość
!= __ne__ Nierówność

Pozostałe metody magiczne, wraz z przykładami użycia, można znaleźć np. tutaj.

💥 Zadanie 3 💥

Zdefiniuj klasę Vector, która będzie przechowywała dwie współrzędne: x i y. Zdefiniuj metody pozwalające na dodawanie, odejmowanie i mnożenie wektorów. Metody te powinny zwracać nowy obiekt klasy Vector z odpowiednio zmodyfikowanymi współrzędnymi.

Podpowiedź

Zdefiniuj metody __add__, __sub__ i __mul__ w klasie Vector.

💥 Zadanie 4 💥

Zdefiniuj klasę Matrix, która będzie przechowywała dwuwymiarową macierz. Zdefiniuj metody pozwalające na dodawanie, odejmowanie i mnożenie macierzy. Metody te powinny zwracać nowy obiekt klasy Matrix z odpowiednio zmodyfikowanymi wartościami macierzy.

Podpowiedź

Macierz można reprezentować jako listę list. Przykładowa macierz 2x3:

    [[1, 2, 3],
    [4, 5, 6]]

Zdefiniuj metody __add__, __sub__ i __mul__ w klasie Matrix.

Dziedziczenie

Dziedziczenie to mechanizm, który pozwala na tworzenie nowych klas na podstawie już istniejących. Klasa dziedzicząca nazywana jest klasą pochodną, a klasa, z której dziedziczy, nazywana jest klasą bazową. Klasa pochodna dziedziczy atrybuty i metody klasy bazowej. Dzięki dziedziczeniu można unikać powtarzania kodu, a także tworzyć hierarchię klas.

W ramach przykładu wróćmy do klasy Character. Załóżmy, że chcemy stworzyć nową klasę Wizard, która będzie dziedziczyła po klasie Character. Klasa Wizard będzie miała dodatkowy atrybut mana oraz metodę cast_spell. Dzięki dziedziczeniu nie musimy ponownie definiować atrybutów i metod klasy Character.

class Character:
    def __init__(self, name, health, items):
        self.name = name
        self.health = health
        self.items = items

    def introduce(self):
        print("Hello!")

    def is_alive(self):
        return self.health > 0

class Wizard(Character):
    def __init__(self, name, health, items, mana):
        super().__init__(name, health, items)
        self.mana = mana

    def cast_spell(self):
      if self.mana > 0:
        print("Fireball!")
        self.mana -= 1
      else:
        print("Not enough mana!")

Spróbuj utworzyć obiekt klasy Wizard i wywołać metodę cast_spell oraz introduce.

💥 Zadanie 5 💥

Zdefiniuj klasę Warrior, która dziedziczy po klasie Character. Klasa Warrior powinna mieć dodatkowy atrybut strength oraz metodę attack, która wypisuje “Attack!” używając funkcji print oraz zwraca wartość siły (strength). Utwórz obiekt klasy Warrior i wywołaj metodę attack.

Zadanie końcowe

Dana jest klasa bazowa Account, która przechowuje informacje o numerze konta i saldzie.

class Account:
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.balance = balance

    def __str__(self): # Metoda __str__ pozwala na zdefiniowanie reprezentacji obiektu jako string
        return f"Account number: {self.account_number}, balance: {self.balance}"

Przykładowe wykorzystanie:

account1 = SavingsAccount("1234", 1000, 0.05) # numer konta, saldo, oprocentowanie
account2 = CheckingAccount("5678", 500, 3, 5) # numer konta, saldo, liczba darmowych przelewów, opłata za przelew

account1 = account1 + 50
print(account1) # Account number: 1234, balance: 1050.0
account1 = account1 - 50
print(account1) # Account number: 1234, balance: 1000.0

account1.add_interest()
print(account1) # Account number: 1234, balance: 1050.0

account2.transfer(account1, 100)
print(account2) # Account number: 5678, balance: 400
print(account1) # Account number: 1234, balance: 1150.0

Zadanie dla chętnych

Rozbuduj przykład z klasą Character o klasy Knight, Archer i Rogue. Każda z tych klas powinna dziedziczyć po klasie Character i posiadać dodatkowe atrybuty i metody.

Powrót do spisu treści ćwiczeń laboratoryjnych